<template>
  <transition name="picker-fade" v-if="pickedMesh">
    <div id="picker"></div>
  </transition>
</template>

<script>
import * as THREE from "three";
import * as dat from "dat.gui";

import { COMMIT_COMMAND } from "../store/events";
import { commandDataEncoder } from "../utils/DataEncoder";
import { deepClone } from "../utils/Tools";

export default {
  name: "ThreePicker",
  props: {
    camera: {
      default: undefined,
      type: THREE.Camera,
    },
    container: {
      default: undefined,
      type: HTMLElement,
    },
    geometries: {
      default: () => [],
      type: Array,
    },
    render: {
      default: () => {},
      type: Function,
    },
  },
  data() {
    return {
      pickedMesh: null,
      command: "",
      gui: null,
      guiParams: {
        ID: -1,
        type: "",
        positionX: 0,
        positionY: 0,
        positionZ: 0,
        colorMaterial: "#ffffff",
        colorLine: "#000000",
        rotationX: 0,
        rotationY: 0,
        rotationZ: 0,
        length: 0,
        width: 0,
        height: 0,
      },
      savedGuiParams: {},
    };
  },
  mounted() {
    window.test2 = () => {
      return this;
    };
  },
  destroyed() {
    this.gui && this.gui.destroy();
  },
  watch: {
    // null -> DOM
    container() {
      this.initRaycaster();
    },
  },
  methods: {
    // 初始化 GUI面板
    initGUI() {
      this.gui && this.gui.destroy();
      this.gui = new dat.GUI({ name: "My GUI" });

      // 开启存储模式
      // this.gui.remember(this.guiParams);

      const gui = this.gui;
      gui.domElement.style.position = "fixed";
      gui.domElement.style.left = "10px";
      gui.domElement.style.top = "10px";

      // ID 信息
      // const id = this.geometries.indexOf(this.pickedMesh);
      gui.add(this.guiParams, "ID").name("Mesh ID");

      // 位置信息
      const position = gui.addFolder("Position");
      position.add(this.guiParams, "positionX", -50, 50).name("X").step(1); // 步长
      position.add(this.guiParams, "positionY", -50, 50).name("Y").step(1);
      position.add(this.guiParams, "positionZ", -50, 50).name("Z").step(1);

      // 颜色信息
      const color = gui.addFolder("Color");
      color.addColor(this.guiParams, "colorMaterial").name("material");
      color.addColor(this.guiParams, "colorLine").name("line");

      // 旋转信息
      const rotation = gui.addFolder("Rotation");
      rotation.add(this.guiParams, "rotationX", 0, 360).name("X").step(1);
      rotation.add(this.guiParams, "rotationY", 0, 360).name("Y").step(1);
      rotation.add(this.guiParams, "rotationZ", 0, 360).name("Z").step(1);

      // 大小信息: 针对正方体
      const scale = gui.addFolder("Scale");
      scale.add(this.guiParams, "length", 0, 50).step(1);
      scale.add(this.guiParams, "width", 0, 50).step(1);
      scale.add(this.guiParams, "height", 0, 50).step(1);

      // 控制器增加统一回调函数
      for (let key in gui.__folders) {
        const folder = gui.__folders[key];
        folder.__controllers.forEach((controller) => {
          // 数据双向绑定，但导致输入框无法输入
          // controller.listen();

          // 动态更新数据时的回调函数
          controller.onChange(() => {
            this.updateMeshProps(folder.name);
            this.render();
          });

          // 完成命令后回写的回调函数
          controller.onFinishChange(() => {
            try {
              if (this.command !== "") {
                this.savedGuiParams = deepClone(this.guiParams);
                // dispatch 无法进行错误捕捉
                this.$store.dispatch(COMMIT_COMMAND, {
                  command: commandDataEncoder(this.command),
                  onlySend: true,
                });
              }
            } catch (error) {
              console.error(error);
            }
            this.command = "";
          });
        });
      }
    },

    // 初始化射线响应事件
    initRaycaster() {
      let activeColor = 10671776;
      let lastPick = null;
      let lastColor = null;
      let mousePos = { x: -1, y: -1 };
      this.container.addEventListener("mousedown", (event) => {
        mousePos.x = event.clientX;
        mousePos.y = event.clientY;
      });
      this.container.addEventListener("mouseup", (event) => {
        // 鼠标点击事件
        if (
          Math.abs(mousePos.x - event.clientX) > 5 ||
          Math.abs(mousePos.y - event.clientY) > 5
        )
          return;

        this.pickedMesh = this.raycasterPicker(
          event,
          this.container,
          this.camera,
          this.geometries
        );

        // 取消重复选中
        if (this.pickedMesh === lastPick) return;

        // 若颜色未更改，则还原颜色
        if (lastPick) {
          const color = lastPick.material.color.getHex();
          color === activeColor && lastPick.material.color.set(lastColor);
        }

        // 更新状态
        if (this.pickedMesh) {
          lastPick = this.pickedMesh;
          lastColor = this.pickedMesh.material.color.getHex();

          this.pickedMesh.material.color.set(activeColor);
          this.updateGUIParams();
          this.initGUI();
        } else {
          lastPick = null;
          lastColor = null;
          this.gui && this.gui.destroy();
          this.gui = null;
        }
        this.render();
      });
    },

    // 更新并存储 GUI面板初始值值
    updateGUIParams() {
      if (this.pickedMesh) {
        const mesh = this.pickedMesh;
        // 设置ID和类型
        this.guiParams.ID = this.geometries.indexOf(mesh);
        this.guiParams.type = mesh.geometry.type;

        // 设置位置
        this.guiParams.positionX = mesh.position.x;
        this.guiParams.positionY = mesh.position.y;
        this.guiParams.positionZ = mesh.position.z;

        // 设置颜色
        this.guiParams.colorMaterial = "#" + mesh.material.color.getHexString();
        const line = mesh.children.find(
          (child) => child instanceof THREE.LineSegments
        );
        this.guiParams.colorLine = line
          ? "#" + line.material.color.getHexString()
          : "#000000";

        // 设置旋转
        this.guiParams.rotationX = mesh.rotation._x;
        this.guiParams.rotationY = mesh.rotation._y;
        this.guiParams.rotationZ = mesh.rotation._z;

        // 设置大小
        if (mesh?.scale) {
          this.guiParams.length = mesh.scale.x;
          this.guiParams.width = mesh.scale.y;
          this.guiParams.height = mesh.scale.z;
        }

        // 存储初始值
        this.savedGuiParams = deepClone(this.guiParams);
      }
    },

    // 更新属性值
    updateMeshProps(name) {
      // const that = this;
      const mesh = this.pickedMesh;
      const that = this;
      const updateHash = {
        Position() {
          const {
            positionX: px,
            positionY: py,
            positionZ: pz,
          } = that.guiParams;
          const {
            positionX: x,
            positionY: y,
            positionZ: z,
          } = that.savedGuiParams;
          // 判断是否改变，未改变不执行
          if (px === x && py === y && pz === z) return;

          mesh.position.set(px, py, pz);

          that.command = `alter translation ${[
            that.guiParams.ID,
            px - x,
            py - y,
            pz - z,
          ].join(",")}`;
        },
        Color() {
          const { colorLine, colorMaterial } = that.guiParams;
          const { colorLine: savedColorLine, colorMaterial: savedColorMate } =
            that.savedGuiParams;
          const line = mesh.children.find(
            (child) => child instanceof THREE.LineSegments
          );
          // 未改变不执行
          if (colorLine === savedColorLine && colorMaterial === savedColorMate)
            return;

          if (line && colorLine !== that.savedGuiParams.colorLine) {
            line.material.color.set(colorLine);
            that.command = `alter color ${[
              that.guiParams.ID,
              "line",
              colorLine,
            ].join(",")}`;
          } else {
            mesh.material.color.set(colorMaterial);
            that.command = `alter color ${[
              that.guiParams.ID,
              "face",
              colorMaterial,
            ].join(",")}`;
          }
        },
        Rotation() {
          const radToAngle = (r) => Math.floor(((r / Math.PI) * 180) % 360);
          const {
            rotationX: rx,
            rotationY: ry,
            rotationZ: rz,
          } = that.guiParams;
          const {
            rotationX: x,
            rotationY: y,
            rotationZ: z,
          } = that.savedGuiParams;
          // 判断是否改变，未改变不执行
          if (rx === x && ry === y && rz === z) return;

          mesh.rotation.set(rx, ry, rz);

          that.command = `alter rotate ${[
            that.guiParams.ID,
            radToAngle(rx - x),
            radToAngle(ry - y),
            radToAngle(rz - z),
          ].join(",")}`;
        },
        Scale() {
          const { length, width, height } = that.guiParams;
          const {
            length: savedLength,
            width: savedWidth,
            height: savedHeight,
          } = that.savedGuiParams;
          if (
            length === savedLength &&
            width === savedWidth &&
            height === savedHeight
          )
            return;

          mesh.scale.set(length, width, height);

          that.command = `alter scale ${[
            that.guiParams.ID,
            length,
            width,
            height,
          ].join(",")}`;
        },
      };
      if (mesh) {
        updateHash[name]();
      }
    },

    // 射线拾取
    raycasterPicker(event) {
      // 坐标归一化
      const rect = this.container.getBoundingClientRect();
      const position = {
        x: ((event.clientX - rect.left) / rect.width) * 2 - 1,
        y: -((event.clientY - rect.top) / rect.height) * 2 + 1,
      };
      const raycaster = new THREE.Raycaster();
      raycaster.setFromCamera(position, this.camera);
      const intersects = raycaster.intersectObjects(this.geometries);
      for (let intersect of intersects) {
        let mesh = intersect?.object;
        if (this.geometries.indexOf(mesh) !== -1) return mesh;
        // if (mesh.parent instanceof THREE.Mesh) return mesh.parent;
      }
      return undefined;
    },
  },
};
</script>

<style scoped>
#picker {
  position: absolute;
  top: 20px;
  left: 20px;
  z-index: 99;
  height: 200px;
}
</style>
